وقتی ثابتهای شما قانونِ دیگران میشوند
یک بار همکونم از من پرسید چرا در کدم، یک عدد دارای کامنتی به طول سه خط است. آن عدد ۱۸ بود. در کامنت نوشته شده بود که این عدد از چه قوانینی نشأت گرفته، کدام بخش آن را اعلام کرده و اینکه ممکن است بدون هشدار تغییر کند.
یک بار همکارم از من پرسید چرا در کدم، یک عدد دارای کامنتی به طول سه خط است. آن عدد ۱۸ بود.
در کامنت نوشته شده بود که این عدد از چه قانونی آمده، کدام بخش آن را ابلاغ کرده و اینکه ممکن است بدون هشدار تغییر کند. به نظرم برای یک عدد صحیح دو رقمی، تشریفات زیادی به نظر میرسید — تا زمانی که جایگزین آن را به یاد آوردم.
در بیشتر نرمافزارها، یک «مقدار ثابت» (Constant) تصمیمی است که خودتان گرفتهاید. اما در نرمافزاری که قوانین و مقررات را پیادهسازی میکند، مقدار ثابت تصمیمی است که شخص دیگری گرفته، میتواند بدون اطلاع دادن به شما آن را تغییر دهد، و ارزشِ از نظر قانونی صحیحی دارد که شاید در وهله اول آن را درست نخوانده باشید.
من هر دو جنبه این ماجرا را به روشی سخت و با ساخت مجموعهای از ماشینحسابهای مالی یاد گرفتم. در ادامه میگویم چه چیزی اشتباه پیش رفت و سه عادتی را که اکنون در هر کدی که قوانینی متعلق به دیگران را رمزگذاری میکند به کار میبندم، شرح میدهم.
این مقاله برای توسعهدهندگانی است که کدهایی برای پیادهسازی قوانین متعلق به دیگران مینویسند؛ مثل مالیات، حقوق و دستمزد، مزایا، وامدهی، یا هر حوزهای که در آن یک نهاد رگولاتور یا استاندارد، اعداد را تعیین میکند و میتواند بدون اجازه شما آنها را بازنگری کند. اگر تا به حال نرخی را به صورت هاردکد نوشتهاید که بعداً تغییر کرده است، این مقاله برای شماست.
پیشنیازها:
شما به اندازه کافی جاوا اسکریپت و گو (Go) نیاز دارید تا بتوانید قطعهکدهای کوتاه را بخوانید. هر دو در ادامه آمدهاند، اما صرفاً به عنوان نمونه و تصویرسازی. هیچچیز در اینجا به دانش عمیق درباره هیچکدام از این دو زبان وابسته نیست.
من هیچ پیشزمینهای در زمینه مالی یا مالیاتی فرض نمیکنم و هر اصطلاح تخصصی حوزه را در اولین باری که ظاهر میشود، توضیح خواهم داد. مثالها از GST (مالیات بر کالاها و خدمات) هند میآیند؛ زیرا من در آنجا با این باگها برخورد کردم، اما هیچیک از این سه حالت خرابی مختص هند، مالیات یا هیچ حوزه قضایی خاصی نیستند.
آنچه پوشش خواهیم داد:
- باگی که یک عدد قابل قبول برمیگرداند
- مقداری ثابتی که در خواب شما تغییر میکند
- اعتبارسنجیای که در داخل فرم زندگی میکرد
- سه عادت
- جمعبندی
باگی که یک عدد قابل قبول برمیگرداند
بیایید با بدترین نوع آن شروع کنیم، زیرا اصلاً شبیه یک باگ به نظر نمیرسد.
در هند، اگر مالیات بر کالاها و خدمات (GST) را با تأخیر پرداخت کنید، به آن سود تعلق میگیرد. پیادهسازی بدیهی این است: مالیات معوقه را در نرخ سالانه ضرب کنید و بر اساس روزهای تأخیر مقیاسبندی کنید.
const interest = taxOwed * (ratePct / 100) * (days / 365);
این کد از نظر محاسباتی کاملاً بینقص است. با این حال، در تعداد زیادی از موارد واقعی، چندین برابر بیشتر از حد لازم است.
دلیل این امر آن است که «مالیات بدهی» یک عدد واحد نیست. اظهارنامه مالیاتی GST دارای یک بدهی خروجی ناخالص است: مجموع مالیاتِ هر چیزی که فروختهاید. اما بیشتر کسبوکارها بخش زیادی از آن را با اعتبار مالیاتی ورودی (input tax credit)، یعنی مالیاتی که قبلاً روی خریدهای خود پرداخت کردهاند، تسویه میکنند. فقط مابقی آن به صورت نقدی پرداخت میشود. و سود، بر اساس قانون حاکم بر آن، به مالیاتی که در واقعیت به صورت نقدی پرداخت شده تعلق میگیرد، نه به بدهی ناخالص.
بنابراین ما همان فرمول، همان نرخ و دو ورودی مختلف داریم. و یکی از آنها اشتباه است:
90 days late at 18% p.a.
on cash actually paid (₹50,000) → ₹2,219
on gross liability (₹3,00,000) → ₹13,315
شش برابر سود. هر دو عدد همان چیزی هستند که فرمول خروجی میدهد. هیچکدام خطا (Exception) پرتاب نمیکنند. هیچکدام به طور واضح پوچ و غیرمنطقی به نظر نمیرسند. اگر شما توسعهدهنده باشید، تستهایتان پاس میشوند؛ زیرا تستها را بر اساس همان سوءتفاهمی نوشتهاید که کد بر اساس آن نوشته شده است.
این همان حالت خرابی است که کدهای رگولاتوری را متمایز میکند. یک باگ معمولی یک استک تریس (Stack trace) تولید میکند، یا عددی آنقدر نادرست است که بالاخره یک نفر متوجه میشود. اما این باگ، عددی تولید میکند که فرم درستی دارد، در مرتبه بزرگی درستی است و با اعتمادبهنفس کامل اشتباه است.
چه چیزی واقعاً مشکل را حل میکند
یک تست بهتر این مشکل را حل نمیکند. یک کامنت مستندسازی (doc comment) بهتر آن را حل میکند.
راهحل این است که در محل تعریف، یادداشت کنیم که پارامتر به چه کمیتی اشاره دارد و این الزام از کجا آمده است:
// GSTInterest returns interest on a late GST payment.
//
// Under Rule 88B(1) interest runs on the tax actually debited from the
// electronic cash ledger — NOT on the gross output liability. Getting
// this wrong overstates the interest, often by several times. Pass
// cashTaxPaid as the cash-ledger portion, not the gross bill.
//
// Pass annualRatePct as DefaultGSTInterestRate unless the notified rate
// has moved; 18% p.a. is currently notified under Section 50(1).
func GSTInterest(cashTaxPaid float64, days int, annualRatePct float64) float64
توجه داشته باشید که نام پارامتر cashTaxPaid نامیده شده است، نه amount. همین نامگذاری، تمام خط دفاعی ماست. برنامهنویسی که فراخوانی را انجام میدهد و صورتحساب ناخالص را در دست دارد، باید متوقف شود و بپرسد آیا این همان عدد درست است یا خیر (که دقیقاً همان توقفی است که از بروز باگ جلوگیری میکند).
من فراتر میروم: اگر یک پارامتر را بتوان با یک کمیت مشابه اشتباه گرفت، نام آن باید امکان هرگونه اشتباهی را غیرممکن کند. عبارت amount دعوتکننده اشتباه است، اما cashTaxPaid آن را ممنوع میکند.
مقداری ثابتی که در خواب شما تغییر میکند
دومین حالت خرابی، کندتر و شرمآورتر است.
پلههای مالیاتی با بودجه سالانه جابهجا میشوند. سقفهای قانونی با ابلاغیه اصلاح میشوند. یک سقف سنوات (یا پاداش پایان خدمت) سالها روی یک رقم ثابت میماند و ناگهان دیگر اینطور نیست. هیچکدام از این تغییرات اهمیتی به برنامه انتشار (Release) شما نمیدهند.
پیادهسازی وسوسهانگیز استفاده از یک مقدار ثابت (Constant) است:
const gratuityCeiling = 2000000
حالا به این فکر کنید که چه کسی از این موضوع آسیب میبیند. نه تنها کاربری که در حال خواندن یک عدد منسوخ است، بلکه توسعهدهندهای که روز سهشنبه متوجه تغییر میشود، میبیند که مقدار در یک وابستگی (Dependency) پخته و سخت شده است، و کاری از دستش برنمیآید جز اینکه یک ایشو (Issue) ثبت کند و منتظر بماند.
یک مشکل ظریفتر هم وجود دارد. ارقام قانونی اغلب یک مقدار واحد نیستند. آنها برای بیشتر مردم یک مقدار هستند. همان سقف برای برخی از کارمندان دولتی بالاتر است. همان حداقل دوره خدمت برای کارکنان با قرارداد موقت کوتاهتر است. یک مقدار ثابتِ پنهان نه تنها ممکن است منسوخ شده باشد، بلکه برای کلِ یک دسته از کاربران، برای همیشه و به شکلی خاموش، اشتباه است.
بنابراین: مقادیر قانونی را به گزینههایی با مقادیر پیشفرض مستندشده تبدیل کنید، هرگز از مقادیر ثابت پنهان استفاده نکنید.
// GratuityOptions carries the statutory figures that change by
// notification. The zero value means "use the defaults".
type GratuityOptions struct {
Ceiling float64 // 0 -> 20,00,000. Use 25,00,000 for Central Government civil employees.
MinYears float64 // 0 -> 5. Use 1 for fixed-term employees.
}
مقدار صفر همچنان کار درست را برای حالتهای رایج انجام میدهد، بنابراین هیچکس برای شروع کار، مالیاتِ پیچیدگی پرداخت نمیکند. اما روزی که رقم تغییر میکند، فراخوانکننده (Caller) میتواند خودش آن را اصلاح کند – بلافاصله، بدون اینکه منتظر بماند تا من نسخه جدیدی را منتشر کنم.
این بند آخر، نکته اصلی است. وقتی کد شما قانون را رمزگذاری میکند که کنترلی روی آن ندارید، کاربران شما به یک راه جایگزین (Override) نیاز دارند که از چرخه انتشار شما عبور نکند.
اعتبارسنجیای که در داخل فرم زندگی میکرد
مورد سوم در جریان یک ریفکتور (Refactor)چنگش را به من انداخت، و این قابلانتقالترین مورد است.
من تابعی داشتم که جدول استهلاک وام را میساخت: یک سطر برای هر ماه، و گامبهگام در طول مدت وام جلو میرفت. درون یک اپلیکیشن، این کار ایمن بود. فرم یک ویژگی max داشت. رابط کاربری ورودی را محدود میکرد. هیچکس نمیتوانست وام ۲۰۰ ساله بخواهد چون رابط کاربری به او اجازه نمیداد.
سپس محاسبات را به یک کتابخانه منتقل کردم و فرم دیگر وجود نداشت.
rows, _ := Amortisation(principal, rate, months) // months came from where?
حلقه فقط توسط مدت زمانی که به آن داده میشود محدود میشود. با یک مقدار بیمعنی، پاسخ اشتباه نمیدهد یا کند اجرا نمیشود؛ بلکه آنقدر تخصیص حافظه انجام میدهد تا فرآیند فراخواننده از کار بیفتد. باگ من، کرشِ کمبود حافظه (Out-Of-Memory) آنها، در برنامهای که هرگز ندیدهام.
راهحل ساده و بیزرقوبرق است: محافظ را درون تابع قرار دهید.
const MaxMonths = 1200 // 100 years
rows, err := Amortisation(principal, rate, 2000)
// err: months must be at most 1200, got 2000
این سقف بهطور عمدی اغراقآمیز است. طولانیترین وام مسکونی که تاکنون ارائه شده ۳۰ تا ۴۰ سال است، بنابراین سقف ۱۰۰ سال تنها روی ورودیهایی فعال میشود که هرگز قرار نبوده معنی خاصی داشته باشند. این همان ویژگیای است که در چنین محافظی میخواهید: باید بهطور تصادفی غیرقابلدسترس باشد، به طوری که هرگز تبدیل به محدودیتی نشود که کسی دربارهاش بحث کند.
درس کلی بسیار فراتر از یک تابع است. وقتی کتابخانهای را از یک اپلیکیشن استخراج میکنید، تمام اعتبارسنجیهای ورودی را که رابط کاربری بهصورت بیسروصدا برای شما انجام میداد به ارث میبرید، و آن را به شکل کرشِ شخص دیگری دریافت میکنید.
سه عادت
اگر کدی مینویسید که قوانینی را پیادهسازی میکند که خودتان آنها را ننوشتهاید، سه عادت وجود دارد که باید همین حالا اتخاذ کنید:
۱. نام مرجع را در کامنت مستندسازی ذکر کنید.
نه اینکه بنویسید «نرخ». بلکه بنویسید کدام قانون، کدام بخش، و اینکه این مقدار در معرض تغییر است. کامنت بالا بهطور دقیق Rule 88B(1) و Section 50(1) را نام میبرد تا هر کسی که کار من را بررسی میکند بداند آن را با چه چیزی تطبیق دهد، و خوانندهای که مخالف است بتواند به جای اعتماد به من، برود و منبع را مطالعه کند.
۲. مقادیر قانونی (statutory) را به عنوان گزینههایی با مقادیر پیشفرض تعریف کنید، نه به صورت ثابت (constant).
مقدار صفر حالت عمومی را پوشش میدهد. مقدار جایگزین (override)، روزی را پوشش میدهد که قانون تغییر میکند و دستهای از کاربران را پوشش میدهد که مقدار پیشفرض هرگز برای آنها مناسب نبوده است.
۳. محافظ را از فرم به تابع منتقل کنید.
هر اعتبارسنجی که رابط کاربری انجام میدهد، چیزی است که کتابخانه شما انجام نمیدهد. فرض کنید فراخواننده بعدی اصلا رابط کاربری ندارد.
هیچکدام از اینها عجیبوغریب نیست. این همان انضباط معمولیِ اعتمادنکردن به ورودیهاست، که در حوزهای اعمال میشود که در آن تعاریف ورودیها توسط شخص دیگری تعیین شده و در زمانبندی خودشان بازبینی میشوند.
دلیل اینکه ارزش دارد این موضوع را بهطور جداگانه نام ببریم، نشانهی خطای خاصِ این حوزه است: نه استثنایی (exception)، نه پشتهی ردیابی (stack trace) و نه خروجی بهطور آشکار اشتباهی. فقط عددی که فرمت درستی دارد اما شش برابر بزرگتر از حد لازم است، و آرام درون گزارشی نشسته که قرار است کسی بر اساس آن دست به اقدام بزند.
جمعبندی
نتیجهگیری کلی این است.
کدی که مقررات را پیادهسازی میکند، نشانهی خطای مختص به خود را دارد: نه استثنا یا پشتهی ردیابی، بلکه فقط عددی که درست به نظر میرسد و با اطمینان کامل اشتباه است.
ما این مسئله را در سه حالت دیدیم: پارامتری که بیسروصدا معنایی متفاوت از آنچه قانون در نظر دارد داشت، یک مقدار قانونی که در قالب یک مقدار ثابت فریز شده بود در حالی که قانون بعداً تغییر کرد، و اعتبارسنجیای که وقتی منطق به یک کتابخانه منتقل شد، در رابط کاربری باقی ماند.
سه عادت ذکر شده به این سه خطا به ترتیب پاسخ میدهند. مرجع را در محل تعریف نامگذاری کنید تا خواننده بعدی بداند کار شما را با چه چیزی بررسی کند. مقادیر قانونی را به عنوان گزینههایی با مقادیر پیشفرض ارائه دهید تا روزی که قانون تغییر کرد، به جای جستجوی یک مقدار ثابت (literal)، یک آرگومان را تغییر دهید. و محافظ را درون تابع قرار دهید، زیرا فراخواننده بعدی ممکن است اصلاً رابط کاربری نداشته باشد.
اگر قرار است فقط یک نکته را به خاطر بسپارید، بگذارید نامگذاری باشد. نام `amount` باگ را دعوت میکند. `cashTaxPaid` از آن جلوگیری میکند. و نام پارامتر ارزانترین مستنداتی است که در طول عمرتان خواهید نوشت.
از خواندن شما متشکرم!
من EMICalcs را ساختهام؛ مجموعهای از ماشینحسابهای مالی رایگان برای هند — شامل وامها، SIP، GST، پاداش پایان خدمت (gratuity) و مالیات. فرمولهای بالا از آن استخراج شده و به عنوان یک کتابخانه منبعباز منتشر شدهاند، بنابراین محاسبات برای هر کسی که بخواهد کار من را بررسی کند قابل حسابرسی است.
نظر مهندس بهمن آبادی: در کدهای رگولاتوری، بزرگترین خطر، پرتاب نشدن خطا نیست، بلکه تولید اعداد کاملاً معتبری است که بهصورت پنهانی اشتباه محاسبه شدهاند. هاردکد کردن مقادیر قانونی و وابستگی به اعتبارسنجیهای لایه UI، بمبهای ساعتی نرمافزار هستند که با تغییر قوانین منفجر میشوند. بهعنوان یک مدرس، توصیه میکنم بهجای مقادیر ثابت، از گزینههای قابل بازنویسی (Override) با مقادیر پیشفرض استفاده کنید و اعتبارسنجیها را همیشه درون تابع محصور نگه دارید.
منبع: https://www.freecodecamp.org/news/what-to-do-when-your-constants-are-someone-else-s-law/